package dnl.toolbox.dto;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A converters registry. The registry is also responsible of automatic
 * converter resolution based on the DTO naming convention 'MyClass' ->
 * 'MyClassDto'.
 * 
 * @author daniel
 * 
 */
public class ConvertersRegistry {

	private Map<ConverterKey, DtoConverter> convertersMap = new HashMap<ConverterKey, DtoConverter>();
	private Map<Class<?>, List<DtoConverter>> mapByClass = new HashMap<Class<?>, List<DtoConverter>>();

	private Set<String> inclusionPackagePrefixesFilter = new HashSet<String>();

	private static final Logger logger = LoggerFactory.getLogger(ConvertersRegistry.class);

	public ConvertersRegistry() {
		inclusionPackagePrefixesFilter.add("com.mzm");
	}

	/**
	 * Registers the a converter.
	 * 
	 * @param dtoConverter
	 */
	public void registerConverter(DtoConverter dtoConverter) {
		List<DtoConverter> aConverters = mapByClass.get(dtoConverter
				.getClassA());
		List<DtoConverter> bConverters = mapByClass.get(dtoConverter
				.getClassB());
		if (aConverters == null) {
			aConverters = new ArrayList<DtoConverter>();
			mapByClass.put(dtoConverter.getClassA(), aConverters);
		} else {
			logger.info("Multiple converters for class: "
					+ dtoConverter.getClassA().getName());
		}
		// /
		if (bConverters == null) {
			bConverters = new ArrayList<DtoConverter>();
			mapByClass.put(dtoConverter.getClassB(), aConverters);
		} else {
			logger.info("Multiple converters for class: "
					+ dtoConverter.getClassB().getName());
		}
		aConverters.add(dtoConverter);
		bConverters.add(dtoConverter);

		convertersMap.put(new ConverterKey(dtoConverter.getClassA(),
				dtoConverter.getClassB()), dtoConverter);
	}

	/**
	 * Attempts to get a converter using a single class only. This is done by
	 * attempting to find a class that matches the naming convention 'MyClass' ->
	 * 'MyClassDto' and then creating a matching <code>DynamicConverter</code>.
	 * 
	 * @param clax
	 * @return
	 */
	public DtoConverter getConverter(Class<?> clax) {
		if (!included(clax)) {
			return null;
		}
		List<DtoConverter> converters = mapByClass.get(clax);
		if (converters == null) {
			DtoConverter converter = loadConverter(clax);
			registerConverter(converter);
			return converter;
		}
		if (converters.size() > 1) {
			throw new UnsupportedOperationException(
					"Multiple converters for class " + clax.getName()
							+ ", cannot resolve.");
		}
		return converters.get(0);
	}

	/**
	 * Gets a converter that can convert between the given types.
	 * 
	 * @param classA
	 * @param classB
	 * @return
	 */
	public DtoConverter getConverter(Class<?> classA, Class<?> classB) {
		if (!included(classA) || !included(classB)) {
			return null;
		}
		DtoConverter dtoConverter = convertersMap.get(new ConverterKey(classA,
				classB));
		if (dtoConverter == null) {
			dtoConverter = new DynamicConverter(classA, classB);
			registerConverter(dtoConverter);
		}
		return dtoConverter;
	}

	private boolean included(Class<?> clax) {
		for (String packagePrefix : inclusionPackagePrefixesFilter) {
			if (clax.getName().startsWith(packagePrefix)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Loads a converter according to a naming convention. The convention is
	 * that a domain object class must have a DTO class in the same package with
	 * the suffix 'Dto' and a converter with the suffix 'DtoConverter'. For
	 * example class mypackage.A must have a DTO named mypackage.ADto and a
	 * converter named mypackage.ADtoConverter.
	 * 
	 * @param c
	 * @return
	 */
	private DtoConverter loadConverter(Class<?> c) {
		boolean isDtoClass = c.getName().endsWith("Dto");
		Class<?> dtoClass;
		Class<?> domainClass;

		if (isDtoClass) {
			dtoClass = c;
			String cName = c.getName();
			cName = cName.substring(0, cName.length() - 3);
			try {
				domainClass = Class.forName(cName);
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
				return null;
			}
		} else {
			domainClass = c;
			String cName = c.getName() + "Dto";
			try {
				dtoClass = Class.forName(cName);
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
				return null;
			}
		}
		return new DynamicConverter(domainClass, dtoClass);
	}
}
